package proy1sistdistribuidos.modelo.seguridad;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.Socket;
import java.security.AlgorithmParameterGenerator;
import java.security.AlgorithmParameters;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import javax.crypto.spec.DHParameterSpec;
import proy1sistdistribuidos.modelo.ControladorExclusion;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 * Clase IntercambiadorLlavesDF
 * Clase que permite acordar el intercambio de llaves de encripcion entre dos partes
 * @author Carlos Diaz Maya, Juan Felipe Garcia
 */
public class IntercambiadorLlavesDF implements IntercambiadorLlaves {
    private static HashMap<String, InformacionAcuerdoLlaves> llavesIntercambiadas;
    private static IntercambiadorLlavesDF solitario;
    private static final String ALGORITMOAUTILIZAR = "DES";


    private IntercambiadorLlavesDF(){
        llavesIntercambiadas = new HashMap<String, InformacionAcuerdoLlaves>();
    }

    public static IntercambiadorLlaves obtenerSolitario() {
        if(solitario == null){
            solitario = new IntercambiadorLlavesDF();
        } 
        return solitario;
    }
    /**
     * Metodo que genera valores aleatorios como parametros del algortimo Diffie-Hellman
     * Fuente del metodo original: http://www.exampledepot.com/egs/javax.crypto/GenDhParams.html
     * @return
     */
    private List<Object> generarParametrosDH() {
    try {
            // Se crea una instancia del generador de parametros para un par de llaves DH de 1024 bits
            AlgorithmParameterGenerator paramGen = AlgorithmParameterGenerator.getInstance("DH");
            paramGen.init(1024);

            // Se generan los parametros
            AlgorithmParameters params = paramGen.generateParameters();
            DHParameterSpec dhSpec
            = (DHParameterSpec)params.getParameterSpec(DHParameterSpec.class);

            // Se devuelven los valores en una lista
            List<Object> retorno = new LinkedList<Object>();

            retorno.add(dhSpec.getP());
            retorno.add(dhSpec.getG());
            retorno.add(dhSpec.getL());

            return retorno;
        } catch (NoSuchAlgorithmException e) {
        } catch (InvalidParameterSpecException e) {
        }
        return null;
    }

    public void enviarIntercambioLlaves(String idSesion) throws Exception {
         try {
            //Se crea la estructura de datos y se ingresa al mapa
            InformacionAcuerdoLlaves info = new InformacionAcuerdoLlaves();

            // Usar valores para generar llaves KeyPairGenerator
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH");
            List<Object> parametrosDH = generarParametrosDH();
            BigInteger q = (BigInteger) parametrosDH.get(0);
            BigInteger g = (BigInteger) parametrosDH.get(1);
            Integer l = (Integer) parametrosDH.get(2);
            DHParameterSpec dhSpec = new DHParameterSpec(q, g, l);
            keyGen.initialize(dhSpec);
            info.setQ(q);
            info.setG(g);
            info.setL(l);
            KeyPair keypair = keyGen.generateKeyPair();
            // Conseguir llaves pública y privada
            PrivateKey privateKey = keypair.getPrivate();
            info.setLlavePrivada(privateKey);
            PublicKey publicKey = keypair.getPublic();
            // Enviar llave publica a la otra parte
            byte[] publicKeyBytes = publicKey.getEncoded();

            byte[] llavePublicaQueEnvio = publicKeyBytes;
            info.setLlavePublicaQueEnvio(publicKeyBytes);

            //Se manda por el sockete...
            InformacionSocketDF infoAEnviar = new InformacionSocketDF();
            infoAEnviar.setId(idSesion);
            infoAEnviar.setDato(llavePublicaQueEnvio);
            infoAEnviar.setDato1(q);
            infoAEnviar.setDato2(g);

            //Se extrae la direccion ip de destino
            String[] partesIdSesion = idSesion.split("_");
            
            String direccionIPDestino = partesIdSesion[1];

            InformacionSocketDF result = this.enviarComandoIntercambioLlave(direccionIPDestino, infoAEnviar);

            if(result == null) throw new Exception("No se recibio la llave publica");

            //..........
            // Recibir la llave publica de otra parte
            byte[] llavePublicaQueRecibo = result.getDato();

            byte[] publicKeyBytesR = llavePublicaQueRecibo; // Obtener el objeto de llave publica
            info.setLlavePublicaQueRecibo(llavePublicaQueRecibo);
            X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKeyBytesR);
            KeyFactory keyFact = KeyFactory.getInstance("DH");
            PublicKey publicKeyR = keyFact.generatePublic(x509KeySpec);
            // Prepararse para generar la llave
            KeyAgreement ka = KeyAgreement.getInstance("DH");
            ka.init(privateKey);
            ka.doPhase(publicKeyR, true);
            // Se especifica el tipo de llave a generar (Se va a usar Blowfish)
            String algorithm = ALGORITMOAUTILIZAR;
            // Generar la llave secreta
            SecretKey secretKey = ka.generateSecret(algorithm);
            info.setLlaveAcordada(secretKey);
            
            //Se inserta la informacion del acuerdo en el mapa
            llavesIntercambiadas.put(idSesion, info);
        } catch(Exception f){
            f.printStackTrace();
        }
    }

    @Override
    public InformacionSocketDF recibirIntercambioLlaves(InformacionSocketDF informacion) {
       try {
            //Se crea la estructura de datos y se ingresa al mapa
            InformacionAcuerdoLlaves info = new InformacionAcuerdoLlaves();
            
            // Usar valores para generar llaves KeyPairGenerator
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH");
            BigInteger q = (BigInteger) informacion.getDato1();
            BigInteger g = (BigInteger) informacion.getDato2();
            Random entero = new Random(System.currentTimeMillis());
            Integer l = entero.nextInt(10)+1;   //aleatorio entre 1 y 11
            DHParameterSpec dhSpec = new DHParameterSpec(q, g, l);
            keyGen.initialize(dhSpec);
            info.setQ(q);
            info.setG(g);
            info.setL(l);
            KeyPair keypair = keyGen.generateKeyPair();
            // Conseguir llaves pública y privada
            PrivateKey privateKey = keypair.getPrivate();
            info.setLlavePrivada(privateKey);
            PublicKey publicKey = keypair.getPublic();
            // Enviar llave publica a la otra parte
            byte[] publicKeyBytes = publicKey.getEncoded();

            byte[] llavePublicaQueEnvio = publicKeyBytes;
            info.setLlavePublicaQueEnvio(publicKeyBytes);

            //Informacion para retornar al equipo que hizo el acuerdo
            InformacionSocketDF infoAEnviar = new InformacionSocketDF();
            infoAEnviar.setId(informacion.getId());
            infoAEnviar.setDato(llavePublicaQueEnvio);

            //..........
            // Recibir la llave publica de otra parte
            byte[] llavePublicaQueRecibo = informacion.getDato();

            byte[] publicKeyBytesR = llavePublicaQueRecibo; // Obtener el objeto de llave publica
            info.setLlavePublicaQueRecibo(llavePublicaQueRecibo);
            X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKeyBytesR);
            KeyFactory keyFact = KeyFactory.getInstance("DH");
            PublicKey publicKeyR = keyFact.generatePublic(x509KeySpec);
            // Prepararse para generar la llave
            KeyAgreement ka = KeyAgreement.getInstance("DH");
            ka.init(privateKey);
            ka.doPhase(publicKeyR, true);
            // Se especifica el tipo de llave a generar (Se va a usar Blowfish)
            String algorithm = ALGORITMOAUTILIZAR;
            // Generar la llave secreta
            SecretKey secretKey = ka.generateSecret(algorithm);
            info.setLlaveAcordada(secretKey);
            //Guardar la informacion de acuerdo en el mapa
            llavesIntercambiadas.put(informacion.getId(), info);
            // Usar la llave secreta para encriptar ................
            return infoAEnviar;
        } catch(Exception f){
            f.printStackTrace();
            return null;
        }
    }


    @Override
    public SecretKey obtenerLlaveEncripcionDesencripcionAcordada(String idSesion) {
        return llavesIntercambiadas.get(idSesion).getLlaveAcordada();
    }

    private InformacionSocketDF enviarComandoIntercambioLlave(String ip, InformacionSocketDF infoAEnviar) throws IOException {
        Socket elSockete = new Socket(ip, ControladorExclusion.puertoSocketes);

         String cadenaXML = infoAEnviar.aXMLString(); //Se serializa la informacion de acuerdo DH a XML

         BASE64Encoder codifBase64 = new BASE64Encoder();
         String cadenaXMLBase64 = codifBase64.encode(cadenaXML.getBytes());
         cadenaXMLBase64 = cadenaXMLBase64.replaceAll("\r\n", "_CHARRO2_");
         cadenaXMLBase64 = cadenaXMLBase64.replaceAll("\n", "_CHARRO_");
         String comando = "EnviarAcuerdo_"+cadenaXMLBase64;
         PrintWriter pregunta = new PrintWriter(elSockete.getOutputStream(), true);
         BufferedReader respuesta = new BufferedReader(new InputStreamReader(elSockete.getInputStream()));

         //Se envia el comando al nodo
         pregunta.println(comando);

         //Se recibe la respuesta del nodo
         String respNodo = respuesta.readLine();


         if (respNodo != null && !respNodo.equals("")) {
            //Se recibe los datos del acuerdo codificados en Base64
            BASE64Decoder decodeBase64 = new BASE64Decoder();
            String respNodoProce = respNodo.replaceAll("_CHARRO2_", "\r\n");
            respNodoProce = respNodoProce.replaceAll("_CHARRO_", "\n");
            byte[] cadenaXMLEstadoRecibidoBytes = decodeBase64.decodeBuffer(respNodoProce);
            String cadenaXMLEstadoRecibido = new String(cadenaXMLEstadoRecibidoBytes);
            elSockete.close();
            InformacionSocketDF retorno = new InformacionSocketDF(cadenaXMLEstadoRecibido);
            return retorno;
        } else {
             return null;
        }

    }
}
